package de.stefanocke.roo.wicket.base

import de.stefanocke.roo.wicket.RooStereotype
import de.stefanocke.roo.xtend.ItdBuilder
import java.lang.reflect.Modifier
import org.springframework.roo.classpath.details.IdentifiableAnnotatedJavaStructure
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata
import org.springframework.roo.model.JavaSymbolName
import org.springframework.roo.model.JavaType
import org.springframework.roo.classpath.details.AbstractIdentifiableAnnotatedJavaStructureBuilder
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails
import de.stefanocke.roo.wicket.RooFactoryMethod
import org.springframework.roo.classpath.details.MethodMetadata
import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue
import org.springframework.roo.classpath.details.annotations.ClassAttributeValue
import java.util.List
import java.util.Collections
import de.stefanocke.roo.wicket.model.JavaTypeMetadataDetails

import static de.stefanocke.roo.xtend.Method.*
import static de.stefanocke.roo.xtend.MethodSignature.*
import static de.stefanocke.roo.xtend.Parameter.*

import static extension de.stefanocke.roo.xtend.Annotation.*
import static extension de.stefanocke.roo.xtend.RooExtensions.*
import static extension de.stefanocke.roo.wicket.base.WicketItdBuilder.*

class WicketItdBuilder extends ItdBuilder {
	public static String SPRING_BEAN = "org.apache.wicket.spring.injection.annot.SpringBean"
	
	public static JavaType FACTORY_METHOD_ANNOTATION = typeof(RooFactoryMethod).javaType
	
	public static String IMODEL = "org.apache.wicket.model.IModel"
	
	public static JavaType COMPONENT = "org.apache.wicket.Component".javaType
	
	public static JavaType ENUM = typeof(Enum).javaType
	
	public static JavaType STEREOTYPE_ANNOTATION = typeof(RooStereotype).javaType
	
	//true means, that there are some fields with SpringBean annotation
	//Note: If buildFields has not yet been called, this will always return false
	def boolean needsInjection(){
		return itdBuilder.declaredFields.exists [ annotations.exists[ hasType(SPRING_BEAN) ] ]
	}
	
	def callInjectorIfRequired(){
		if(needsInjection){
			'''«"org.apache.wicket.injection.Injector".resolveStatic».get().inject(this);'''
		}
	}
	
	override de.stefanocke.roo.wicket.base.ItdMetadata getMetadata(){
		super.metadata as de.stefanocke.roo.wicket.base.ItdMetadata
	}	
	
	def static fqnRooStereotype(){
		STEREOTYPE_ANNOTATION.fqn
	}
		
	override addMemberTypeAdditions(){
		if(super.addMemberTypeAdditions()){
			replaceSpringAnnotations()
			true
		} else false
	}
	
	//Wicket-Spring integration does not support @Autowired annotation, but only it's own SpringBean annotation.
	//Thus, replace all @Autowired by @SpringBean.
	//For now we only consider fields. Constructor injection isn't supportes by wicket-spring anyway...
	def replaceSpringAnnotations(){
		itdBuilder.declaredFields.forEach [it.replaceSpringAnnotations];
	}
	
	def replaceSpringAnnotations(AbstractIdentifiableAnnotatedJavaStructureBuilder<?> annotated){
		val replacedAnnotations = annotated.annotations.map[it.replaceSpringAnnotation]
		annotated.setAnnotations(replacedAnnotations);
	}
	
	def replaceSpringAnnotation(AnnotationMetadataBuilder annotation){
		val AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"
		if(annotation.hasType(AUTOWIRED)){ 
			annotation(SPRING_BEAN)  //TODO: Map attribute values
			
		} else 
		annotation
	}
	
	def Iterable<MethodMetadata> getFactoryMethods(){
		val List<MethodMetadata> methods = newArrayList()
		methods.addAll(govTypeDetails.factoryMethods) 
		//since we are just introducing the base class, it is not yet available in inheritance hierarchy
		if(baseClassStereotype != null){
			//TODO: Don't do this, if the class has a manual supertype
			methods.addAll(baseClassStereotype.stereotypeDetails.factoryMethods)
		}
		methods
	}
	
	def getFactoryMethods(ClassOrInterfaceTypeDetails cid){
		cid.getMethods().filter [it.factoryMethodAnnotation != null]
	}
	
	
	def getBestMatchingFactoryMethod(JavaType javaType, JavaTypeMetadataDetails details, List<JavaType> annotations){
		val typeDetails = if(details == null) new JavaTypeMetadataDetails(javaType) else details;
		
		//at first, the methods are considered, where the type matches		
		val typeMatchingMethods = factoryMethods
							.sortBy [it.factoryMethodTypeDistance(typeDetails)]
							.filter[it.factoryMethodTypeDistance(typeDetails) < Integer::MAX_VALUE]
							.toList;  
			 
		//then, as a fallback, the methods are considered, where no type is specified
		typeMatchingMethods.addAll(factoryMethods.filter [it.factoryMethodTypeWildcard]);
		
		//in any case, we only accept a method, when all its "requiredAnnotations" are also on the field
		//TODO: sort the result further descending by the number of annotations that match.
		//That is: The factory method wich requires the most annotations wins (and the property must have all of those).
		//Main purpose: factory method with no annotations is fallback.
		typeMatchingMethods.filter [annotations.containsAll(it.factoryMethodAnnotationsAttribute.map[value])].head
	}
	
	def factoryMethodTypeDistance(MethodMetadata mm, JavaTypeMetadataDetails typeDetails){		
		 val typeDistances = mm.factoryMethodTypesAttribute.map[typeDetails.typeDistance(it.value)]
		 if(typeDistances.empty){Integer::MAX_VALUE} else Collections::min(typeDistances);
	}
	
 	//Integer::MAX_VALUE means the types are not related
 	//0 means the types are equal
 	//>0 means assigableFrom is true (TODO: And the value shall express the distance then)
	def typeDistance(JavaTypeMetadataDetails typeDetails, JavaType otherType){
		val type = typeDetails.javaType.wrapIfPrimitive
		if(type.equals(otherType) || typeDetails?.enumType && ENUM.equals(otherType)){
			0
		} else if(otherType.coreType && type.coreType){
				//since roo does not support reflection for non-application types,
					//we only support instanceof for java core types here.
					//TODO: Support at least some instanceof for application types as well...
			val cl = this.getClass.classLoader;
			if(cl.loadClass(otherType.fqn).isAssignableFrom(cl.loadClass(type.fqn))){
				1
			} else {
				Integer::MAX_VALUE
			}
		} else Integer::MAX_VALUE
	}
	
	
	
	def factoryMethodTypeWildcard(MethodMetadata mm){
		 mm.factoryMethodTypesAttribute.empty	
	}
	
	def List<ClassAttributeValue> factoryMethodTypesAttribute(MethodMetadata mm){
		val attr = mm.factoryMethodAnnotation.getAttribute("modelClasses") 			
		if(attr==null) newArrayList() 
			else { 
				if(attr instanceof ArrayAttributeValue) (attr as ArrayAttributeValue).value 	
				else newArrayList(attr as ClassAttributeValue)
			}
	}
	def List<ClassAttributeValue> factoryMethodAnnotationsAttribute(MethodMetadata mm){
		val attr = mm.factoryMethodAnnotation.getAttribute("requiredAnnotations")	
		if(attr==null) newArrayList() 
			else { 
				if(attr instanceof ArrayAttributeValue) (attr as ArrayAttributeValue).value 	
				else newArrayList(attr as ClassAttributeValue)
			}	
	}
	
	
	def getFactoryMethodAnnotation(MethodMetadata mm){
		mm.getAnnotation(FACTORY_METHOD_ANNOTATION);
	}
	
	
	
	def getModelClassAware(){
		if(metadata instanceof ModelClassAwareMetadata){
			metadata as ModelClassAwareMetadata
		}
	}
	
	def getModelClass(){modelClassAware?.modelClass}
	
	def getModelClassTypeDetails(){modelClassAware?.modelClassTypeDetails}
	
	def stereotype(String s){
		metadata.stereotypes.get(s)?.name
	}
	def stereotypeDetails(String s){
		metadata.stereotypes.get(s)
	}

	def static stereotypeAnnotation(IdentifiableAnnotatedJavaStructure annotated){
		annotated.annotations.findFirst(a | a.annotationType.fqn == fqnRooStereotype)
	}
	
	def static hasStereotype(IdentifiableAnnotatedJavaStructure ajs, String stereotype){
		
		ajs.stereotypeAnnotation?.value?.equals(stereotype)
	}
	
	def static value(AnnotationMetadata a){
		a?.getAttribute("value".symbol)?.value
	}
	
	def stereotype(String s, JavaType ... typeArg){
		if(typeArg==null) 
			s.stereotype
		else 
			s.stereotype?.fqn.javaType(typeArg)
	}
	
	
	def static findRequiredMethodWithStereotype(ClassOrInterfaceTypeDetails cid, String stereotype){
		val m = cid.declaredMethods.filter[it.hasStereotype(stereotype)].head;
		if(m==null){
			throw new IllegalArgumentException("Method with stereotype " + stereotype + " not found in "+cid.type);
		}
		m
	}
	
	override JavaType getBaseClass(){
		if(baseClassStereotype != null){
			val baseClass=stereotype(baseClassStereotype)
			if(modelClass!=null){
				//Default: the base class is assumed to have one type argument
				//which must be the model class		
				baseClass.fqn.javaType(modelClass)		
			} else {
				baseClass
			}
		
		}
	}
	
	def String baseClassStereotype(){
		
	}
	
	def constructorNoArg(){
		method(
			signature(Modifier::PUBLIC),
			[|'''
			super();
			''']
		)
	}
	
	def constructorWithId(){
		method(
			signature(Modifier::PUBLIC,	param("java.lang.String", "id")
			),
			[|'''
			super(id);
			''']
		)
	}
	
	def constructorWithIdAndMarkupProvider(){
		method(
			signature(Modifier::PUBLIC,	
				param("java.lang.String", "id"),
				param("org.apache.wicket.MarkupContainer", "markupProvider")
			),
			[|'''
			super(id, markupProvider);
			''']
		)
	}
	
	def constructorWithModel(){
		method(
			signature(Modifier::PUBLIC,
				modelParam(modelClass,"model")
			),
			[|'''
			super(model);
			''']
		)
	}
	def constructorWithIdAndModel(){
		method(
			signature(Modifier::PUBLIC, 
				param("java.lang.String", "id"), 
				modelParam(modelClass, "model")
			),
			[|'''
			super(id, model);
			''']
		)
	}
	
	def constructorWithIdModelAndMarkupProvider(){
		method(
			signature(Modifier::PUBLIC, 
				param("java.lang.String", "id"), 
				param("org.apache.wicket.model.IModel".javaType(modelClass),"model"),
				param("org.apache.wicket.MarkupContainer", "markupProvider")
			),
			[|'''
			super(id, model, markupProvider);
			''']
		)
	}
	
	def modelField(JavaType type, JavaSymbolName name){
		field(model(type), name)
	}
	def modelField(JavaType type, String name){
		field(model(type), name.symbol)
	}
	def modelParam(JavaType type, JavaSymbolName name){
		param(model(type), name)
	}
	def modelParam(JavaType type, String name){
		param(model(type), name)
	}
	def model(JavaType type){
		IMODEL.javaType(type)
	}
	def model(String type){
		type.javaType.model
	}
}